package tcp

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"net"
	"strconv"
	"time"

	"gitee.com/dark.H/gs"
	"golang.org/x/net/proxy"
)

const (
	idType  = 0 // address type index
	idIP0   = 1 // ip address start index
	idDmLen = 1 // domain address length index
	idDm0   = 2 // domain address start index

	typeIPv4     = 1 // type is ipv4 address
	typeDm       = 3 // type is domain address
	typeIPv6     = 4 // type is ipv6 address
	typeRedirect = 9

	lenIPv4              = net.IPv4len + 2 // ipv4 + 2port
	lenIPv6              = net.IPv6len + 2 // ipv6 + 2port
	lenDmBase            = 2               // 1addrLen + 2port, plus addrLen
	AddrMask        byte = 0xf
	socksVer5            = 5
	socksCmdConnect      = 1
	socksCmdUdp          = 3
	// lenHmacSha1 = 10
)

var (
	readTimeout time.Duration
)

func Socks5Go(dst gs.Str, timeout ...int) proxy.Dialer {
	if dst.StartsWith("socks5://") {
		dst = dst.Split("socks5://").Last()
	}
	i := 7
	if timeout != nil {
		i = timeout[0]
	}

	d, err := proxy.SOCKS5("tcp", dst.Str(), nil, &net.Dialer{Timeout: time.Duration(i) * time.Second})
	if err != nil {
		gs.Str(err.Error()).Color("r").Println()
		return nil
	}
	return d
}

func TestAlive(network, dst gs.Str, proxystr ...string) bool {

	if proxystr == nil {
		con, err := net.DialTimeout(network.Str(), dst.Trim().Str(), time.Second*7)
		if err != nil {
			return false
		}
		defer con.Close()
		return true

	} else {
		dialer := Socks5Go(gs.Str(proxystr[0]))
		ctx, cancel := context.WithTimeout(context.Background(), time.Duration(7)*time.Second)
		defer cancel()

		con, err := dialer.(proxy.ContextDialer).DialContext(ctx, network.Str(), dst.Trim().Str())
		// con, err := dialer.Dial(network.Str(), dst.Trim().Str())
		if err != nil {
			return false
		}
		defer con.Close()
		return true
	}
}

func SetReadTimeout(c *net.Conn) {
	if readTimeout != 0 {
		(*c).SetReadDeadline(time.Now().Add(readTimeout))
	}
}

func GetServerRequest(conn net.Conn) (host string, raw []byte, isUdp bool, err error) {

	// utils.SetStreamReadTimeout(*conn)
	SetReadTimeout(&conn)
	const (
		idVer   = 0
		idCmd   = 1
		idType  = 3 // address type index
		idIP0   = 4 // ip address start index
		idDmLen = 4 // domain address length index
		idDm0   = 5 // domain address start index

		typeIPv4   = 1 // type is ipv4 address
		typeDm     = 3 // type is domain address
		typeIPv6   = 4 // type is ipv6 address
		typeChange = 5 // type is ss change config

		lenIPv4   = 3 + 1 + net.IPv4len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv4 + 2port
		lenIPv6   = 3 + 1 + net.IPv6len + 2 // 3(ver+cmd+rsv) + 1addrType + ipv6 + 2port
		lenDmBase = 3 + 1 + 1 + 2           // 3 + 1addrType + 1addrLen + 2port, plus addrLen
	)
	// buf size should at least have the same size with the largest possible
	// request size (when addrType is 3, domain name has at most 256 bytes)
	// 1(ver) + 1(cmd) + 1(0) + 1(addrType) + 1(lenByte) + 255(max length address) + 2(port) + 10(hmac-sha1)
	// g := color.New(color.FgBlue)
	buf := make([]byte, 269)
	// read till we get possible domain length field

	if _, err = io.ReadFull(conn, buf[:idType+1]); err != nil {
		return
	}
	if buf[idVer] != 0x5 {
		err = errors.New("not socks5 type")
	}
	if buf[idCmd] == socksCmdUdp {
		isUdp = true
	} else if buf[idCmd] == socksCmdConnect {

	} else {
		err = errors.New("not socks5 conn or udp")
	}

	if err != nil {
		buf1 := make([]byte, 1401)
		copy(buf1, buf[:idType+1])
		pre := len(buf[:idType+1])
		n, err2 := conn.Read(buf1[idType+1:])
		if err2 != nil {
			// ColorL("copy err:", err)
			return
		}
		// ColorL("buf:", buf[:idType+1])
		// ColorL("buffer:", buf1[:n+pre])

		raw = buf1[:n+pre]
		return
	}

	// g.Printf("read %v \n", buf[:20])
	var reqStart, reqEnd int
	addrType := buf[idType]
	// ColorL("addrType:", addrType&AddrMask)
	switch addrType & AddrMask {
	case typeIPv4:
		reqStart, reqEnd = idIP0, lenIPv4
		raw = buf[:reqEnd]
	case typeIPv6:
		reqStart, reqEnd = idIP0, idIP0+lenIPv6
		raw = buf[:reqEnd]
	case typeDm:
		if _, err = io.ReadFull(conn, buf[idType+1:idDmLen+1]); err != nil {
			return
		}
		reqStart, reqEnd = idDm0, int(buf[idDmLen])+lenDmBase
		raw = buf[:reqEnd]
		// ColorL("Raw:", raw)
	case typeRedirect:

		if _, err = io.ReadFull(conn, buf[idType+1:idDmLen+1]); err != nil {
			return
		}
		// g.Printf("read %v \n", buf[:20])
		reqStart, reqEnd = idDm0, idDm0+int(buf[idDmLen])
		raw = buf[:reqEnd]
	default:

		// fmt.Println("Err buf:", buf)
		err = fmt.Errorf("addr type %d not supported:%s", addrType&AddrMask, buf)
		return
	}

	if _, err = io.ReadFull(conn, buf[reqStart:reqEnd]); err != nil {
		// g.Printf("read %v \n", buf[:100])
		return
	}
	// data = buf[:reqEnd]
	// Return string for typeIP is not most efficient, but browsers (Chrome,
	// Safari, Firefox) all seems using typeDm exclusively. So this is not a
	// big problem.
	switch addrType & AddrMask {
	case typeIPv4:
		host = net.IP(buf[idIP0 : idIP0+net.IPv4len]).String()
	case typeIPv6:
		host = net.IP(buf[idIP0 : idIP0+net.IPv6len]).String()
	case typeDm:
		host = string(buf[idDm0 : idDm0+int(buf[idDmLen])])
	case typeRedirect:
		host = string(buf[idDm0 : idDm0+int(buf[idDmLen])])
		return

	default:
		err = errors.New("error in typoe")
	}

	// parse port
	port := binary.BigEndian.Uint16(buf[reqEnd-2 : reqEnd])
	host = net.JoinHostPort(host, strconv.Itoa(int(port)))
	// raw = buf[:reqEnd]
	return
}
